


#include "bulletinboard.h"

#include "constants.h"
#include "writebuffer.h"

#include <unistd.h>

#include <algorithm>
#include <iostream>
#include <fstream>
#include <sstream>

#include <cctype>
#include <climits>


using namespace std;


extern pthread_mutex_t write_buffer_mutex;
extern pthread_cond_t write_buffer_cond;
extern int last_write_result;


namespace
{
    // A convenience method to convert strings to integers.
    int intFromString( const string & s )
    {
        istringstream iss( s );
        int i;
        if ( !(iss >> i) )
            return INT_MIN;
        return i;
    }
}


BBMessage::BBMessage( int id, const string & user, const string & message )
  : m_id( id ),
    m_user( user ),
    m_text( message )
{
}


BBMessage::BBMessage()
  : m_id( 0 ),
    m_user(),
    m_text()
{
}


int BBMessage::id() const
{
    return m_id;
}


string BBMessage::user() const
{
    return m_user;
}


string BBMessage::text() const
{
    return m_text;
}





BulletinBoard::BulletinBoard()
  : m_loaded( false ),
    m_lastUsedId( 0 ),
    m_fileName( "bb.db" )
{
    pthread_rwlock_init( &m_rwlock, 0 );
}


BulletinBoard::~BulletinBoard()
{
    pthread_rwlock_destroy( &m_rwlock );
}


void BulletinBoard::setFileName( const string & file )
{
    pthread_rwlock_wrlock( &m_rwlock );
    // Throw away all the current info.
    m_fileName = file;
    m_loaded = false;
    m_messages.clear();
    m_lastUsedId = 0;

    // Create the file if it doesn't exist.
    ofstream ofs( m_fileName.c_str(), ostream::out | ostream::app );
    ofs.close();
    pthread_rwlock_unlock( &m_rwlock );

    load();
}


string BulletinBoard::interpretCommand( const string & command, string & user, bool & done )
{
    const char ws[] = " \t\r\n";
    int start = command.find_first_not_of( ws );

    // Ignore empty lines.
    if ( start == string::npos )
        return string();

    ostringstream reply;

    // Find the command name.
    int endOfword = command.find_first_of( ws, start );
    string firstWord = command.substr( start, endOfword - start );
    transform( firstWord.begin(), firstWord.end(), firstWord.begin(), (int(*)(int))toupper );

    // Find the rest of the command.
    string rest;
    int startOfRest = command.find_first_not_of( ws, endOfword );
    if ( startOfRest != string::npos )
        rest = command.substr( startOfRest );

    if ( firstWord == "QUIT" )
    {
        done = true;
        reply << "BYE: Come back soon." << endl;
    }
    else if ( rest.empty() )
    {
        reply << "ERROR SYNTAX: No arguments given to " << firstWord << '.' << endl;
    }
    else if ( firstWord == "USER" )
    {
        user = rest;
        reply << "HELLO " << user << ": Welcome to the BBServ board." << endl;
    }
    else if ( firstWord == "READ" )
    {
        int id = intFromString( rest );
        BBMessage m;
        int result = read( id, m );
        if ( result == IdNotFound )
            reply << "UNKNOWN " << id << ": No message was found on the board with that ID." << endl;
        else if ( result == ErrorOpeningForReading )
            reply << "ERROR READ: Could not open the database file for reading." << endl;
        else if ( result == ErrorWhileReading )
            reply << "ERROR READ: Error encountered while reading from database file." << endl;
        else
            reply << "MESSAGE: " << m.id() << ' ' << m.user() << '/' << m.text() << endl;
    }
    else if ( firstWord == "WRITE" )
    {
        ostringstream oss;
        oss << "-1/" << user << '/' << rest;

        int result = WriteQueue::self()->submitMessage( pthread_self(), oss.str() );

        if ( result == ErrorOpeningForReading )
            reply << "ERROR WRITE: Could not open the database file for reading." << endl;
        else if ( result == ErrorWhileReading )
            reply << "ERROR WRITE: Error encountered while reading from database file." << endl;
        else if ( result == ErrorOpeningForWriting )
            reply << "ERROR WRITE: Could not open the database file for writing." << endl;
        else if ( result == ErrorWhileWriting )
            reply << "ERROR WRITE: Error encountered while writing to database file." << endl;
        else if ( result == ErrorSendingToOtherServers )
            reply << "ERROR WRITE: Message couldn't be synchronised with other servers. "
                     "Message has been dropped. Please try resending later." << endl;
        else
            reply << "WROTE " << result << endl;
    }
    else if ( firstWord == "REPLACE" )
    {
        int slashPos = rest.find('/');
        if ( slashPos == string::npos )
        {
            reply << "ERROR SYNTAX: The message ID and message text were not separated by a slash." << endl;
        }
        else
        {
            int id = intFromString( rest.substr( 0, slashPos ) );

            BBMessage m;
            if ( read( id, m ) == IdNotFound )
            {
                reply << "UNKNOWN " << id << ": No message was found on the board with that ID." << endl;
            }
            else
            {
                ostringstream oss;
                oss << id << '/' << user << '/' << rest.substr( slashPos + 1 );

                int result = WriteQueue::self()->submitMessage( pthread_self(), oss.str() );

                if ( result == IdNotFound )
                    reply << "UNKNOWN " << id << ": No message was found on the board with that ID." << endl;
                else if ( result == ErrorOpeningForReading )
                    reply << "ERROR REPLACE: Could not open the database file for reading." << endl;
                else if ( result == ErrorWhileReading )
                    reply << "ERROR REPLACE: Error encountered while reading from database file." << endl;
                else if ( result == ErrorOpeningForWriting )
                    reply << "ERROR REPLACE: Could not open the database file for writing." << endl;
                else if ( result == ErrorWhileWriting )
                    reply << "ERROR REPLACE: Error encountered while writing to database file." << endl;
                else if ( result == ErrorSendingToOtherServers )
                    reply << "ERROR REPLACE: Message couldn't be synchronised with other servers. "
                            "Message has been dropped. Please try resending later." << endl;
                else
                    reply << "WROTE " << id << endl;
            }
        }
    }
    else
    {
        reply << "ERROR SYNTAX: Unrecognized command " << firstWord << '.' << endl;
    }

    return reply.str();
}


int BulletinBoard::undoLastWrite()
{
    pthread_rwlock_wrlock( &m_rwlock );
    int result = truncate( m_fileName.c_str(), m_undoFileSize );
    m_loaded = false;
    pthread_rwlock_unlock( &m_rwlock );

    load();
    return result;
}


int BulletinBoard::writeMessage( const string & line )
{
    int result = load();
    if ( result != NoError )
        return result;

    int firstSlash = line.find( '/' );
    int secondSlash = line.find( '/', firstSlash + 1 );
    if ( firstSlash == string::npos || secondSlash == string::npos )
        return ErrorParsingLine;

    istringstream iss( line.substr( 0, firstSlash ) );
    int id;
    iss >> id;
    string user = line.substr( firstSlash + 1, secondSlash - firstSlash - 1 );
    string text = line.substr( secondSlash + 1 );
    BBMessage m( id, user, text );

    pthread_rwlock_wrlock( &m_rwlock );

    ofstream ofs( m_fileName.c_str(), ios_base::out | ios_base::app );
    if ( ofs.fail() )
    {
        result = ErrorOpeningForWriting;
        m_loaded = false;
    }
    else
    {
        m_undoFileSize = ofs.tellp();

        ofs << line << endl;

        if ( ofs.fail() )
        {
            result = ErrorWhileWriting;
            m_loaded = false;
        }
        else
        {
            m_messages[id] = m;
            result = id;

            if ( id > m_lastUsedId )
                m_lastUsedId = id;
        }
    }

    pthread_rwlock_unlock( &m_rwlock );

    return result;
}


int BulletinBoard::lastMessageId()
{
    pthread_rwlock_rdlock( &m_rwlock );
    int result = m_lastUsedId;
    pthread_rwlock_unlock( &m_rwlock );
    return result;
}


int BulletinBoard::load()
{
    int result = NoError;

    pthread_rwlock_rdlock( &m_rwlock );
    bool loaded = m_loaded;
    pthread_rwlock_unlock( &m_rwlock );
    if ( loaded )
        return result;

    pthread_rwlock_wrlock( &m_rwlock );

    ifstream ifs( m_fileName.c_str() );
    if ( ifs.fail() )
    {
        result = ErrorOpeningForReading;
    }
    else
    {
        m_messages.clear();

        string line;
        while ( getline( ifs, line ) )
        {
            if ( ifs.fail() )
            {
                result = ErrorWhileReading;
                break;
            }

            int firstSlash = line.find( '/' );
            int secondSlash = line.find( '/', firstSlash + 1 );

            istringstream iss( line.substr( 0, firstSlash ) );
            int id;
            iss >> id;

            if ( id > m_lastUsedId )
                m_lastUsedId = id;

            string user = line.substr( firstSlash + 1, secondSlash - firstSlash - 1 );
            string message = line.substr( secondSlash + 1 );

            m_messages[id] = BBMessage( id, user, message );
        }

        m_undoFileSize = ifs.tellg();
    }

    m_loaded = result == NoError;

    pthread_rwlock_unlock( &m_rwlock );

    return result;
}


int BulletinBoard::read( int messageId, BBMessage & message )
{
    int result = load();
    if ( result != NoError )
        return result;

    pthread_rwlock_rdlock( &m_rwlock );

    map<int,BBMessage>::const_iterator it = m_messages.find( messageId );
    if ( it != m_messages.end() )
        message = (*it).second;
    else
        result = IdNotFound;

    pthread_rwlock_unlock( &m_rwlock );

    return result;
}

